JavaScript Hook 技术实战指南

作为前端分析、爬虫开发、安全测试的核心工具链之一,JavaScript Hook 能让你在网页运行时「钻空子」——拦截函数调用、插入监控代码、甚至修改执行逻辑。本文将从原理讲起,结合 Tampermonkey 脚本实战,带你快速掌握这项技能。


1. 什么是 Hook 技术

简单来说,Hook(钩子)就是在程序运行时替换或包装原始函数的技术:你可以在函数执行前、执行中、执行后插入自己的代码,同时(可选地)保留原函数的全部或部分功能。

它的核心作用场景

  1. 分析前端加密:截获登录token、密码加密前的明文
  2. 调试前端逻辑:给特定条件的函数调用自动加断点
  3. 修改页面行为:屏蔽广告、解锁付费内容(仅用于学习测试哦)
  4. 监控API调用:追踪fetch/XHR的请求/响应参数

2. 浏览器端最推荐的Hook工具:Tampermonkey

在浏览器环境下做JS Hook,Tampermonkey(油猴) 是当之无愧的首选:

  • 支持所有主流浏览器(Chrome/Edge/Firefox/Safari)
  • 脚本安装和管理零门槛
  • 内置丰富的GM_* API(跨域请求、存储、DOM操作等)
  • 可以指定脚本的运行时机和生效域名

快速安装

  1. Chrome/Edge用户直接去官方应用商店搜索「Tampermonkey Beta」(Beta版功能更全)
  2. Firefox用户去AMO搜索「Tampermonkey」
  3. 官网备用:https://www.tampermonkey.net/

3. Tampermonkey 基础脚本开发

油猴脚本本质是包裹在自执行函数里的普通JS,加上一段元数据头部告诉浏览器脚本的基本信息。

完整的基础模板

// ==UserScript==
// @name         Hook入门测试脚本
// @namespace    https://github.com/yourname/
// @version      1.0.0
// @description  一个演示console.log Hook的油猴脚本
// @author       你的名字
// @match        https://*.example.com/*
// @match        https://login1.scrape.center/*
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict'; // 强制开启严格模式,避免变量污染

    // 这里写你的Hook逻辑
    console.log('✅ Hook入门测试脚本已加载');
})();

高频元数据指令速查表

指令作用常用示例
@match脚本生效的URL模式(支持通配符)@match https://*.jd.com/*
@run-at脚本的执行时机document-start(DOM树前)/document-end(DOM树后)
@grant申请的GM_* API权限GM_xmlhttpRequest(跨域请求)/none(无权限)
@require引入外部JS库@require https://code.jquery.com/jquery-3.7.1.min.js

4. JS Hook 实战基础:三种常用模式

4.1 简单函数Hook(替换/包装)

这是最基础的Hook方法:先保存原始函数的引用,再用自定义函数替换原位置,自定义函数里调用原始函数并插入代码。

/**
 * 通用简单Hook函数
 * @param {object} obj - 原始函数所在的对象(全局函数传window)
 * @param {string} methodName - 要Hook的函数名
 */
function simpleHook(obj, methodName) {
    const originalFn = obj[methodName];
    // 替换原函数
    obj[methodName] = function(...args) {
        // ✅ 执行前逻辑
        console.group(`🔍 Hook捕获到 ${methodName}`);
        console.log('传入参数:', args);
        
        // 调用原始函数(保留原功能)
        const result = originalFn.apply(this, args);
        
        // ✅ 执行后逻辑
        console.log('返回结果:', result);
        console.groupEnd();
        
        return result; // 返回原函数的结果(可选修改)
    };
}

// 测试:Hook全局的console.log
simpleHook(window.console, 'log');

4.2 原型链方法Hook

很多前端API(比如XHR、Canvas)的方法是挂在构造函数的prototype上的,Hook原型可以拦截所有实例的调用。

/**
 * 通用原型链Hook函数
 * @param {function} constructor - 构造函数(比如XMLHttpRequest)
 * @param {string} methodName - 要Hook的原型方法名
 */
function prototypeHook(constructor, methodName) {
    const originalFn = constructor.prototype[methodName];
    constructor.prototype[methodName] = function(...args) {
        console.group(`🎯 原型链Hook ${constructor.name}.${methodName}`);
        console.log('当前实例:', this);
        console.log('传入参数:', args);
        
        const result = originalFn.apply(this, args);
        
        console.log('返回结果:', result);
        console.groupEnd();
        
        return result;
    };
}

// 测试:Hook所有XMLHttpRequest的open方法
prototypeHook(XMLHttpRequest, 'open');

4.3 条件断点Hook

给符合特定条件的函数调用自动加debugger断点,再也不用手动点断点找位置了!

/**
 * 通用条件断点Hook函数
 * @param {object} obj - 原始函数所在的对象
 * @param {string} methodName - 要Hook的函数名
 * @param {function} condition - 判断是否触发断点的函数,参数是原函数的参数
 */
function conditionalBreakpointHook(obj, methodName, condition) {
    const originalFn = obj[methodName];
    obj[methodName] = function(...args) {
        // 符合条件就触发断点
        if (condition(...args)) {
            debugger;
            console.warn(`⚠️ 条件断点触发: ${methodName}`);
        }
        return originalFn.apply(this, args);
    };
}

// 测试:当localStorage.setItem的key是'token'或'password'时触发断点
conditionalBreakpointHook(
    window.localStorage,
    'setItem',
    (key) => ['token', 'password'].includes(key)
);

5. 实战案例:分析 scrape 登录页的token生成

我们以 **https://login1.scrape.center/**(崔庆才老师的爬虫练习站)为例,看看怎么用Hook找到登录token的生成逻辑。

5.1 前期观察

  1. 打开网站,F12切到Network面板
  2. 随便输入用户名密码(比如admin/123456)点击登录
  3. 找到login请求,发现Form Data里只有一个加密的token字段,没有明文密码

5.2 推测与Hook

  • 加密后的token看起来像Base64编码
  • 浏览器端做Base64编码的常用API是window.btoa()
  • 我们直接Hook btoa,顺便打印调用栈找到调用它的函数!

5.3 完整脚本

// ==UserScript==
// @name         Scrape登录Token Hook
// @namespace    https://github.com/yourname/
// @version      1.0.0
// @description  分析scrape.center登录token的生成
// @author       你的名字
// @match        https://login1.scrape.center/*
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // 保存原始btoa
    const originalBtoa = window.btoa;
    // 替换btoa
    window.btoa = function(input) {
        console.group('🔑 btoa Token Hook');
        console.log('加密前的明文:', input);
        console.trace('调用栈(往上翻找到加密逻辑!)');
        debugger; // 自动触发断点,方便调试
        
        const result = originalBtoa(input);
        
        console.log('加密后的token:', result);
        console.groupEnd();
        
        return result;
    };
})();

5.4 分析结果

安装脚本后刷新网站,再次输入密码登录:

  1. 控制台会打印加密前的明文:{"username":"admin","password":"123456"}
  2. 调用栈里可以找到是login函数调用了btoa
  3. 原来就是把用户名密码JSON序列化后直接Base64编码!

6. 进阶技巧与反Hook对抗

6.1 异步函数Hook(Promise/async-await)

现在的前端API(比如fetchaxios.get)大多是异步的,Hook异步函数需要注意错误捕获返回Promise的处理

/**
 * 通用异步函数Hook
 * @param {object} obj - 原始函数所在的对象
 * @param {string} methodName - 要Hook的异步函数名
 */
function asyncHook(obj, methodName) {
    const originalFn = obj[methodName];
    obj[methodName] = async function(...args) {
        const startTime = performance.now();
        console.group(`⚡ 异步Hook ${methodName}`);
        console.log('传入参数:', args);
        
        try {
            const result = await originalFn.apply(this, args);
            console.log('✅ 执行成功,耗时:', (performance.now() - startTime).toFixed(2), 'ms');
            console.log('返回结果:', result);
            return result;
        } catch (err) {
            console.error('❌ 执行失败:', err);
            throw err; // 抛出错误,不影响原逻辑的错误处理
        } finally {
            console.groupEnd();
        }
    };
}

// 测试:Hook全局的fetch API
asyncHook(window, 'fetch');

6.2 属性访问Hook(getter/setter)

如果想监控某个对象的属性被赋值或读取的情况,不能用函数Hook,要用Object.defineProperty的getter和setter。

/**
 * 通用属性访问Hook
 * @param {object} obj - 属性所在的对象
 * @param {string} propName - 要Hook的属性名
 */
function propertyHook(obj, propName) {
    let internalValue = obj[propName]; // 用内部变量存储属性值
    Object.defineProperty(obj, propName, {
        get() {
            console.log(`📖 读取属性 ${propName}:`, internalValue);
            return internalValue;
        },
        set(newValue) {
            console.log(`✏️ 修改属性 ${propName}:`, newValue);
            internalValue = newValue;
        },
        configurable: true, // 必须设为true,否则无法再次修改属性
        enumerable: true // 保持原属性的可枚举性
    });
}

// 测试:Hook window.location.href
propertyHook(window.location, 'href');

6.3 简单的反Hook对抗

现在很多网站会做反Hook:

  1. 检查API是否被修改:比如把btoatoString()和预期的对比
  2. 冻结对象/属性:用Object.freeze()Object.seal()或设置writable: false
  3. 代码混淆:让你找不到Hook点

简单的应对策略

// 1. 从iframe里获取「干净」的原始API(如果网站有iframe的话)
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
const cleanBtoa = iframe.contentWindow.btoa;
// 使用cleanBtoa...

// 2. 绕过writable: false的限制(重新defineProperty)
function bypassWritable(obj, propName, newFn) {
    Object.defineProperty(obj, propName, {
        value: newFn,
        writable: true,
        configurable: true,
        enumerable: true
    });
}

7. 最佳实践

  1. 精准控制作用域:用@match只在需要的网站运行脚本
  2. 最小权限原则:只申请必要的@grant权限,比如不需要跨域就设为none
  3. 做好错误处理:给Hook逻辑加try-catch,避免脚本崩溃影响原网站
  4. 性能优先:不要在高频调用的函数(比如requestAnimationFrame、Canvas的draw方法)里做太复杂的逻辑
  5. 代码可复用:把常用的Hook方法封装成通用函数

8. 总结

本文带你从0到1掌握了浏览器端JS Hook的核心技能:

  • 什么是Hook、它能做什么
  • Tampermonkey的基础使用和脚本开发
  • 三种常用的Hook模式(简单函数、原型链、条件断点)
  • 实战分析scrape登录页的token生成
  • 异步函数、属性访问Hook和简单的反Hook对抗

掌握这些后,你就可以开始分析更复杂的网站前端逻辑啦!记得仅用于学习测试,不要用于非法用途哦~